#include "processor.h"
#include "command.h"

#define QT_NO_DEBUG_OUTPUT 1

#include <QPainterPathStroker>
#include <QDebug>
#include <QBrush>
#include <QPen>
#include <QtMath>

namespace LeGerber
{

	Processor::Processor(QObject *parent) : QObject(parent)
	{
		ApertureTemplate *apertureTemplate = new CircleApertureTemplate;
		m_apertureTemplateDictionary.insert(apertureTemplate->name, apertureTemplate);
		apertureTemplate = new RectApertureTemplate;
		m_apertureTemplateDictionary.insert(apertureTemplate->name, apertureTemplate);
		apertureTemplate = new ObroundApertureTemplate;
		m_apertureTemplateDictionary.insert(apertureTemplate->name, apertureTemplate);
		apertureTemplate = new PolygonApertureTemplate;
		m_apertureTemplateDictionary.insert(apertureTemplate->name, apertureTemplate);
	}

	Processor::~Processor()
	{
		qDeleteAll(m_apertureTemplateDictionary.values());
		qDeleteAll(m_apertureDictionary.values());
		qDeleteAll(m_objects);
	}

	void Processor::setInterpolation(Interpolation interpolation)
	{
		m_interpolation = interpolation;
		return;
	}

	void Processor::setQuadrant(Quadrant quadrant)
	{
		m_quadrant = quadrant;
		return;
	}

	void Processor::setUnit(Unit unit)
	{
		if (m_unit != InvalidUnit)
		{
			//qDebug() << "Warning changing unit twice";
		}
		m_unit = unit;
	}

	void Processor::setPolarity(Polarity polarity)
	{
		m_polarity = polarity;
	}

	bool Processor::addCustomApertureTemplate(CustomApertureTemplate *customTemplate)
	{
		if (customTemplate == nullptr)
		{
            error(QString("Invalid aperture template"));
			return false;
		}
		if (m_apertureTemplateDictionary.contains(customTemplate->name))
		{
			error(QString("Aperture template already defined: '%1'").arg(customTemplate->name));
			return false;
		}
		m_apertureTemplateDictionary.insert(customTemplate->name, customTemplate);
		return true;
	}

	bool Processor::addAperture(int number, const QString &templateName, const QList<qreal> &templateParameters)
	{
		if (!m_apertureTemplateDictionary.contains(templateName)) // Use NullApertureTemplate?
		{
			error(QString("Aperture template not found: '%1'").arg(templateName));
			return false;
		}

		if (m_apertureDictionary.contains(number))
		{
			error(QString("Aperture already defined: '%1'").arg(number));
			return false;
		}

		auto aperture = m_apertureTemplateDictionary.value(templateName)->instanciate(templateParameters);
		if (aperture == nullptr)
		{
			error(QString("Cannot instanciate aperture: '%1' with template '%2'").arg(number).arg(templateName));
			return false;
		}
		aperture->apertureNumber = number;
		m_apertureDictionary.insert(aperture->apertureNumber, aperture);
		return true;
	}

	void Processor::addComment(const QString &text)
	{
		//qDebug() << "COMMENT:" << text;
	}

	void Processor::beginRegion()
	{
		m_inRegion = true;
		m_currentRegion = QPainterPath();
	}

	void Processor::endRegion()
	{
		m_regions.append(m_currentRegion);
		for (auto region: m_regions)
		{
			if (region.isEmpty())
				continue;

			auto object = new Object;
			object->painterPath = region;
			object->painterPath.closeSubpath();
			object->polarity = m_polarity;
			m_objects.append(object);
		}
		m_inRegion = false;
		m_regions.clear();
	}

	void Processor::endSession()
	{

	}

	bool Processor::setAperture(quint32 number)
	{
		if (!m_apertureDictionary.contains(number)) // Use NullAperture?
		{
			error("Aperture not found");
			return false;
		}
		m_currentAperture = m_apertureDictionary.value(number);
		return true;
	}

	bool Processor::stroke(const QPointF &to, const QPointF &distance)
	{
		QPointF startPoint = m_currentPosition;
		QPointF endPoint = mergeCoordinates(to, m_currentPosition);

		switch (m_interpolation)
		{
			case InvalidInterpolation:
			case LinearInterpolation:
			{
				if (m_interpolation == InvalidInterpolation)
				{
					warning("Using default linear interpolation (not standard)");
					m_interpolation = LinearInterpolation;
				}

				if (!m_inRegion)
				{
					if (m_currentAperture == nullptr)
					{
						error("Linear stroking w/o current aperture");
						return false;
					}

					if (startPoint != endPoint)
					{
						// FIXME: only C and R aperture allowed, but we only support C
						if (m_currentAperture->apertureTemplate->name != "C")
						{
							error(QString("Unsupported aperture with linear interpolation: '%1'").arg(m_currentAperture->apertureTemplate->name));
							return false;
						}
						auto object = new Object;
						QPainterPath line;
						line.moveTo(startPoint);
						line.lineTo(endPoint);
						object->painterPath = strokePath(line);
						object->polarity = m_polarity;
						m_objects.append(object); // FIXME: could compose the path, and stroke it at the end (move or end)
					}
					else
					{
						// Same as Flash, can use any aperture
						auto object = new Object;
						object->painterPath = m_currentAperture->shape.translated(m_currentPosition);
						object->polarity = m_polarity;
						m_objects.append(object);
					}
				}
				else
				{
					if (m_currentRegion.isEmpty())
						m_currentRegion.moveTo(startPoint);
					m_currentRegion.lineTo(endPoint);
				}
				break;
			}
			case ClockWiseInterpolation:
			{
				error("Clockwise arcs not supported yet");
				return false;
			}
			case CounterClockWiseInterpolation:
			{
				if (!m_inRegion)
				{
					if (m_currentAperture == nullptr)
					{
						error("Arc stroking w/o current aperture");
						return false;
					}
					if (m_currentAperture->apertureTemplate->name != "C")
					{
						error(QString("Unsupported aperture with arc interpolation: '%1'").arg(m_currentAperture->apertureTemplate->name));
						return false;
					}
				}
				QPointF centerDistance = mergeCoordinates(distance, QPointF(0, 0));

				//            qDebug() << QString("Arc: From [%1,%2] to [%3,%4] with distance [%5,%6]")
				//                        .arg(startPoint.x()).arg(startPoint.y())
				//                        .arg(endPoint.x()).arg(endPoint.y())
				//                        .arg(distance.x()).arg(distance.y());

				// Fig 13.
				if (m_quadrant == SingleQuadrant)
				{
					auto path = arcFromStartEndDistance(startPoint, endPoint, centerDistance);

					if (m_inRegion)
					{
						if (m_currentRegion.isEmpty())
							m_currentRegion.moveTo(startPoint);
					}
					else
					{
						auto object = new Object; // Pos = [0, 0]
						object->painterPath = strokePath(path);
						object->polarity = m_polarity;
						m_objects.append(object);
					}

				}
				else if (m_quadrant == MultipleQuadrant)
				{
					QPointF center = startPoint + centerDistance;
					QLineF r1 = QLineF(center, startPoint);
					QLineF r2 = QLineF(center, endPoint);
					qreal startAngle = r1.angle();
					qreal spanAngle = r1.angleTo(r2);
					if (qFuzzyCompare(spanAngle, 360.0))
						spanAngle = 0.0;
					if (!qFuzzyCompare(r1.length(), r2.length()))
					{
						//warning("Numerical instability");
					}
					qreal radius = r1.length();
					QRectF rect(center.x()-radius, center.y()-radius, radius*2.0, radius*2.0);

					if (m_inRegion)
					{
						if (m_currentRegion.isEmpty())
							m_currentRegion.moveTo(startPoint);
						m_currentRegion.arcTo(rect, startAngle, -360+spanAngle);
					}
					else
					{
						//qDebug() << startPoint << endPoint << center << startAngle << spanAngle << radius << rect;
						QPainterPath path;
						path.moveTo(startPoint);
						path.arcTo(rect, startAngle, -360+spanAngle);

						auto object = new Object; // Pos = [0, 0]
						object->painterPath = strokePath(path);
						object->polarity = m_polarity;
						m_objects.append(object);
					}
				}
			}
		}
		m_currentPosition = endPoint;
		return true;
	}

	QPainterPath Processor::strokePath(const QPainterPath &path)
	{
		QPainterPathStroker stroker;
		stroker.setCapStyle(Qt::RoundCap);
		stroker.setJoinStyle(Qt::RoundJoin);
		stroker.setWidth(m_currentAperture->width);
		return stroker.createStroke(path);
	}

	QPainterPath Processor::arcFromStartEndDistance(const QPointF &start, const QPointF &end, const QPointF &distance)
	{
		QVector<QPointF> centers;
		centers << m_currentPosition + distance
		        << m_currentPosition + QPointF(distance.x(), -distance.y())
		        << m_currentPosition + QPointF(-distance.x(), distance.y())
		        << m_currentPosition + QPointF(-distance.x(), -distance.y());
		qreal startAngle = qSNaN();
		qreal spanAngle = qSNaN();
		QRectF rect;
		for (auto center: centers)
		{
			QLineF r1 = QLineF(center, start);
			QLineF r2 = QLineF(center, end);
			startAngle = r1.angle();
			spanAngle = r1.angleTo(r2);
			//        qDebug() << "CCW SQ C" << center;
			//        qDebug() << "CCW SQ R1" << r1 << r1.angle() << r1.length();
			//        qDebug() << "CCW SQ R2" << r2 << r2.angle() << r2.length();
			//        qDebug() << "CCW SQ A" << startAngle << spanAngle;
			if ((360-spanAngle) >= 0 && (360 - spanAngle) <= 90)
			{
				if (!qFuzzyCompare(r1.length(), r2.length()))
				{
					//warning("Numerical instability");
				}
				qreal radius = r1.length();
				rect = QRectF(center.x()-radius, center.y()-radius, radius*2.0, radius*2.0);
				break;
			}
		}
		QPainterPath path;
		path.moveTo(start);
		path.arcTo(rect, startAngle, -360+spanAngle);
		return path;
	}

	QPointF Processor::mergeCoordinates(const QPointF &newPos, const QPointF &previousPos)
	{
		QPointF pos = newPos;
		if (qIsNaN(pos.x()))
			pos.setX(previousPos.x());
		if (qIsNaN(pos.y()))
			pos.setY(previousPos.y());

		return pos;
	}

	bool Processor::moveTo(const QPointF &pos)
	{
		m_currentPosition = mergeCoordinates(pos, m_currentPosition);

		if (m_inRegion)
		{
			m_regions.append(m_currentRegion);
			m_currentRegion = QPainterPath();
			m_currentRegion.moveTo(m_currentPosition);
		}

		return true;
	}

	bool Processor::flash(const QPointF &pos)
	{
		m_currentPosition = mergeCoordinates(pos, m_currentPosition);

		if (m_currentAperture == nullptr)
		{
			error("Flashing w/o current aperture");
			return false;
		}

		auto object = new Object;
		object->painterPath = m_currentAperture->shape.translated(m_currentPosition);
		object->polarity = m_polarity;
		m_objects.append(object);
		return true;
	}

	QList<Object *> Processor::takeObjects()
	{
		QList<Object *> result;
		result.swap(m_objects);
		return result;
	}

	bool Processor::isError() const
	{
		return !m_errorMessage.isEmpty();
	}

	QString Processor::errorMessage() const
	{
		return m_errorMessage;
	}

	int Processor::warningMessageCount() const
	{
		return m_warningMessages.count();
	}

	QStringList Processor::warningMessages() const
	{
		return m_warningMessages;
	}

	void Processor::error(const QString &text)
	{
		m_errorMessage = text;
	}

	void Processor::warning(const QString &text)
	{
		m_warningMessages.append(text);
	}

}
